package data

import (
	"context"
	"time"

	"github.com/brocaar/chirpstack-api/go/v3/gw"
	"github.com/jmoiron/sqlx"
	"github.com/pkg/errors"
	"network/v3/internal/band"
	"network/v3/internal/config"
	"network/v3/internal/models"
	//"network/v3/internal/roaming"
	"github.com/brocaar/lorawan"
	"network/v3/internal/storage"
)

const defaultCodeRate = "4/5"

type incompatibleCIDMapping struct {
	CID              lorawan.CID
	IncompatibleCIDs []lorawan.CID
}

var incompatibleMACCommands = []incompatibleCIDMapping{
	{CID: lorawan.NewChannelReq, IncompatibleCIDs: []lorawan.CID{lorawan.LinkADRReq}},
	{CID: lorawan.LinkADRReq, IncompatibleCIDs: []lorawan.CID{lorawan.NewChannelReq}},
}

var (
	// rejoin-request variabled
	rejoinRequestEnabled   bool
	rejoinRequestMaxCountN int
	rejoinRequestMaxTimeN  int

	// Class-B
	classBPingSlotDR        int
	classBPingSlotFrequency int

	// RX window
	rxWindow              int
	rx2PreferOnRX1DRLt    int
	rx2PreferOnLinkBudget bool

	// RX2 params
	rx2Frequency int
	rx2DR        int

	// RX1 params
	rx1DROffset int
	rx1Delay    int

	// Downlink TX power
	downlinkTXPower int

	// MAC Commands
	disableMACCommands bool

	// ADR
	disableADR bool

	// ClassC
	classCDownlinkLockDuration time.Duration

	// Dwell time.
	uplinkDwellTime400ms   bool
	downlinkDwellTime400ms bool
	uplinkMaxEIRPIndex     uint8

	// Max mac-command error count.
	maxMACCommandErrorCount int

	// Prefer gateways with min uplink SNR margin
	gatewayPreferMinMargin float64
)

// Setup configures the package.
func Setup(conf config.Config) error {
	nwServer := conf.NetworkServer
	nsConf := nwServer.NetworkSettings
	rejoinRequestEnabled = nsConf.RejoinRequest.Enabled
	rejoinRequestMaxCountN = nsConf.RejoinRequest.MaxCountN
	rejoinRequestMaxTimeN = nsConf.RejoinRequest.MaxTimeN

	classBPingSlotDR = nsConf.ClassB.PingSlotDR
	classBPingSlotFrequency = nsConf.ClassB.PingSlotFrequency

	rx2Frequency = nsConf.RX2Frequency
	rx2DR = nsConf.RX2DR
	rx1DROffset = nsConf.RX1DROffset
	rx1Delay = nsConf.RX1Delay
	rxWindow = nsConf.RXWindow

	rx2PreferOnRX1DRLt = nsConf.RX2PreferOnRX1DRLt
	rx2PreferOnLinkBudget = nsConf.RX2PreferOnLinkBudget

	downlinkTXPower = nsConf.DownlinkTXPower

	disableMACCommands = nsConf.DisableMACCommands
	disableADR = nsConf.DisableADR

	classCDownlinkLockDuration = nwServer.Scheduler.ClassC.DownlinkLockDuration

	uplinkDwellTime400ms = nwServer.Band.UplinkDwellTime400ms
	downlinkDwellTime400ms = nwServer.Band.DownlinkDwellTime400ms

	maxEIRP := nwServer.Band.UplinkMaxEIRP
	if maxEIRP == -1 {
		maxEIRP = band.Band().GetDefaultMaxUplinkEIRP()
	}
	uplinkMaxEIRPIndex = lorawan.GetTXParamSetupEIRPIndex(maxEIRP)

	maxMACCommandErrorCount = nsConf.MaxMACCommandErrorCount
	gatewayPreferMinMargin = nsConf.GatewayPreferMinMargin

	return nil
}

type dataContext struct {
	ctx context.Context

	// Database connection or transaction.
	DB              sqlx.Ext
	DBInTransaction bool

	// Device mode.
	DeviceMode storage.DeviceMode

	// ServiceProfile of the device.
	ServiceProfile storage.ServiceProfile

	// DeviceProfile of the device.
	DeviceProfile storage.DeviceProfile

	// DeviceSession holds the device-session of the device for which to send
	// the downlink data.
	DeviceSession storage.DeviceSession

	// DeviceGatewayRXInfo contains the RXInfo of one or multiple gateways
	// within reach of the device. These gateways can be used for transmitting
	// downlinks.
	DeviceGatewayRXInfo []storage.DeviceGatewayRXInfo

	// MustSend defines if a frame must be send. In some cases (e.g. ADRACKReq)
	// the network-server must respond, even when there are no mac-commands or
	// FRMPayload.
	MustSend bool

	// ACK defines if ACK must be set to true (e.g. the frame acknowledges
	// an uplink frame).
	ACK bool

	// RXPacket holds the received uplink packet (in case of Class-A downlink).
	RXPacket *models.RXPacket

	// Immediately indicates that the response must be sent immediately.
	Immediately bool

	// MACCommands contains the mac-commands to send (if any). Make sure the
	// total size fits within the FRMPayload or FOpts.
	MACCommands []storage.MACCommandBlock

	// DeviceQueueItem contains the possible device-queue item to send to the
	// device.
	DeviceQueueItem *storage.DeviceQueueItem

	// MoreDeviceQueueItems defines if there are more items in the queue besides
	// DeviceQueueItem.
	MoreDeviceQueueItems bool

	// Downlink frame.
	DownlinkFrame gw.DownlinkFrame

	// Downlink frame items. This can be multiple items so that failing the
	// first one, there can be a retry with the next item.
	DownlinkFrameItems []downlinkFrameItem

	// Gateway to use for downlink.
	DownlinkGateway storage.DeviceGatewayRXInfo
}

type downlinkFrameItem struct {
	// Downlink frame item
	DownlinkFrameItem gw.DownlinkFrameItem

	// The remaining payload size which can be used for mac-commands and / or
	// FRMPayload.
	RemainingPayloadSize int
}

// HandleResponse handles a downlink response.
func HandleResponse(ctx context.Context, rxPacket models.RXPacket, sp storage.ServiceProfile, ds storage.DeviceSession, _, mustSend, ack bool, macCommands []storage.MACCommandBlock) error {
	rctx := dataContext{
		ctx:             ctx,
		DB:              storage.DB(),
		DBInTransaction: false,
		ServiceProfile:  sp,
		DeviceSession:   ds,
		ACK:             ack,
		MustSend:        mustSend,
		RXPacket:        &rxPacket,
		MACCommands:     macCommands,
	}

	return HandleResponseRunTasks(&rctx)
}

func HandleResponseRunTasks(rctx *dataContext) error {
	for _, responseTask := range responseTasks {
		if err := responseTask(rctx); err != nil {
			if err == ErrAbort {
				return nil
			}

			return err
		}
	}

	return nil
}

// HandleScheduleNextQueueItem handles scheduling the next device-queue item.
func HandleScheduleNextQueueItem(ctx context.Context, db sqlx.Ext, ds storage.DeviceSession, mode storage.DeviceMode) error {
	nqctx := dataContext{
		ctx:             ctx,
		DB:              db,
		DBInTransaction: true, // the scheduler loop is in transaction as it needs to block the device rows
		DeviceMode:      mode,
		DeviceSession:   ds,
	}

	return HandleScheduleNextQueueItemRunTasks(&nqctx)
}

func HandleScheduleNextQueueItemRunTasks(nqctx *dataContext) error {
	for _, t := range scheduleNextQueueItemTasks {
		if err := t(nqctx); err != nil {
			if err == ErrAbort {
				return nil
			}
			return err
		}
	}

	return nil
}

// This should only happen when the cached device-session is not in sync
// with the actual device-session.
func returnInvalidDeviceClassError(_ *dataContext) error {
	return errors.New("the device is in an invalid device-class for this action")
}

// data errors
var (
	ErrFPortMustNotBeZero     = errors.New("FPort must not be 0")
	ErrFPortMustBeZero        = errors.New("FPort must be 0")
	ErrAbort                  = errors.New("nothing to do")
	ErrNoLastRXInfoSet        = errors.New("no last RX-Info set available")
	ErrInvalidDataRate        = errors.New("invalid data-rate")
	ErrMaxPayloadSizeExceeded = errors.New("maximum payload size exceeded")
)
